bind vs apply vs call en Javascript

bind , apply y call. Estas son funciones que como otros numerosos temas, causan mucha confusión al iniciar tu trayecto en Javascript. Sobre todo si vienes de programar con lenguajes orientados a objetos.

Pero no te preocupes. Te daré primero la explicación express:

Estas 3 funciones sirven para modificar el valor de this de la función que quieras.

Si entendiste esa explicación, perfecto. Tienes claros todos los temas necesarios como prerequisito para esto. Solo saltate al final a la sección de Las funciones para ir directo al grano.

De lo contrario te recomiendo que sigas leyendo.


El problema

La siguiente función le regresa a una persona un premio que consiste en un número al azar de juegos:

function getPrizeGames(prizeLimit) {
    const games = [
        'Resident Evil',
        'Devil May Cry',
        'Metal Gear Solid',
        'Horizon Zero Dawn',
        'God of War',
        'Silent Hill'
    ] // Nuestro catalogo de juegos
    const prizeCount = Math.floor(Math.random() * prizeLimit) // El número de juegos que se ganó
    
    const winnerGames = [] 
    for (let i = 0; i < prizeCount; i++) {
        const gameSelected = games[Math.floor(Math.random() * games.length)]; // Un juego seleccionado al azar
        
        const randomGame = {
            arrivalDate: new Date(), // La fecha de entrega del juego
            gameTitle: gameSelected, // El titulo del juego
            getCongratsMessage: function(winnerName) {
                console.log(`Congratulations ${winnerName}! ${this.gameTitle} will arrive on ${this.arrivalDate}.`)
            } // Una función que regresa un mensaje con la información estructurada para el usuario
        }
        winnerGames.push(randomGame)
    }
    return winnerGames
}

Ahora imagina que llamamos a la función de la siguiente manera y llamamos a getCongratsMessage por cada juego que se regrese:

const games = getPrizeGames(3)
games.forEach(function(game) {
    console.log(game.getCongratsMessage('Juan'))
})

¿Qué resultado se imprimirá en consola?

Muy fácil:

Congratulations Juan! Devil May Cry will arrive on Wed Jan 04 2023 18:52:08 GMT-0600 (Central Standard Time).

Congratulations Juan! Metal Gear Solid will arrive on Wed Jan 04 2023 18:52:08 GMT-0600 (Central Standard Time).

Congratulations Juan! God of War will arrive on Wed Jan 04 2023 18:52:08 GMT-0600 (Central Standard Time).

¿Pero y si le agregamos un timeout? ¿Pasará algo diferente?

const games = getPrizeGames(3)
games.forEach(function(game) {
    setTimeout(game.getCongratsMessage, 3000, 'Jose');
})

Bueno pues la respuesta es… si. Ahora hay algunos undefined.

Congratulations Jose! undefined will arrive on undefined.

Congratulations Jose! undefined will arrive on undefined.

*Recuerda que el numero de juegos es seleccionado al azar y por ende el numero de logs puede variar.

Esto ocurre no por culpa de setTimeout sino porque se utilizó getCongratsMessage como función de callback. Es decir se paso la función como parámetero a otra función para que se use dentro de otro proceso.

Pero…¡Spoiler Alert!

¡Esto se arregla con bind!

Sin embargo antes de pasar a la solución debes entender la razón exacta del porque se pierden las referencias a los valores. Y para ello necesitas tener muy claros algunos conceptos.


Los pre-requisitos

this y el Execution Context

Lo primero que hay que entender es como funciona this y el Execution Context

No ahondaré en todo lo que hay que saber, pero te daré un resumen de lo elemental. Si aún después de esto no te queda claro o quieres saber más, te recomiendo que le heches un vistazo a: Lo que necesitas saber de this en Javascript y a El único ejemplo que necesitas para entender Global Execution Context en Javascript.

Dicho esto empezemos con lo importante.

this es una palabra reservada que normalmente apunta al Execution Context (Contexto de Ejecución) actual. JS cuenta con 2 tipos de Execution Contexts:

  • Global Execution Context
  • Function Execution Context

El Global Execution Context es solo uno y como el nombre lo dice es el que abarca de manera global todo tu código. Los Function Execution Context son todos los demás y puede haber de 0 a n en tus scripts.

Una forma fácil de manejarlo es ver a this (y por lo tanto al Execution Context) como el objeto que manda a llamar una función en particular.

El objeto principal que manda a llamar todo es el objeto global. Puedes ver este comportamiento al trabajar con el código mas simple de JS:

var myString = 'The World'

// La siguiente línea solo es necesaria si no estas corriendo tu código de JS en el navegador
// global.myString = myString 

function callSample() {
    console.log(this.myString)
}

callSample() // The World

this apunta al objeto global y por eso puede acceder a la variable de myString. Podrías mandar a llamar a callSample de las siguientes maneras también: window.callSample() / global.Sample() (El primero es el objeto global del navegador el segundo puede ser de otros engines como NodeJS), pero JS permite que se omita el uso del objeto global explícito al acceder a sus propiedades.

El objeto global puede albergar otros objetos y estos a su vez pueden llamar a más funciones:

var myString = 'The World'

function callSample() {
    console.log(this.myString)
}

var containerObject = {
    myString: 'Star Platinum',
    callSample: callSample
}

containerObject.callSample() // Star Platinum

¿Te diste cuenta? Mande a llamar la misma función en ambos ejemplos. La diferencia es que en el primer caso this trajo el valor del objeto global y en el 2°, this trajo el valor del objeto containerObject.

Objetos y Prototype

Los objetos y los prototype son cada uno otro tema enoooorme en JS. Probablemente incluso más que this o que los Execution Contexts.

Sin embargo la versión express para entender esto es que en JS prácticamente todo es un objeto (a excepción de los valores primitivos). Las funciones son objetos, los arreglos son objetos, los módulos son objetos. Y obviamente… los objetos son objetos.

A su vez todo los los objetos tienen una propiedad intrinseca conocida como protoype. Esta propiedad también es un objeto y por ende tiene su propio prototype. La cadena de prototypes que se hace entre cada objeto se conoce precisamente como la Prototype Chain.

Todos los objetos eventualmente llegan a través de esta cadena a un objeto conocido como Object.prototype. Este es el prototype mas básico y el que permite que los demas objetos “hereden” su comportamiento base.

El comportamiento “heredado” incluye varias variables y funciones. Entre las cuales se encuentran: bind, call y apply.

*Digo “heredado” porque no es que Object.prototype se considere padre de todos los objetos. JS no usa herencia de la manera tradicional que se enseña en Programación Orientada a Objetos. La relación entre objetos tiene una estructura mas similar a una composición aunque tenga el mismo comportamiento aparente de la herencia.


Las funciones

Entendiendo lo anterior podemos entonces platicar acerca de la solución que necesitamos.

El problema que teniamos era que se perdía la referencia a nuestras variables al momento de utilizar nuestra función como una función de callback.

const games = getPrizeGames(3)
games.forEach(function(game) {
    setTimeout(game.getCongratsMessage, 3000, 'Jose');
})

Esto pasa porque setTimeout recibe solo la referencia a la función que se le pasa, no el objeto que la contenía (en este caso game). Por lo tanto cuando se manda a llamar a getCongratsMessage dentro de setTimeout el objeto que lo llama ya no es game sino el objeto global. Y porsupuesto el objeto global no tiene ninguna propiedad que se llame gameTitle o arrivalDate y por lo tanto regresa los valores como undefined.

¡Ufff! Esta pesado ¿no?

La solución al problema es simple. Mantener el Execution Context original al momento de llamar a getCongratsMessage

¿Como hacemos eso? Con bind. Y tal vez con apply y call… tal vez.

Como mencioné al principio: estas 3 funciones sirven para modificar el Execution Context de la función que quieras.

bind

bind recibe como parámetro la nueva referencia a this y nos regresa una copia de nuestra función con esta modificación.

const games = getPrizeGames(3)
games.forEach(function(game) {
    setTimeout(game.getCongratsMessage.bind(game), 3000, 'Julian');
})

Si en el parámetro de bind pasamos el game especifico se guarda entonces una copia de getCongratsMessage que apunte a ese objeto game.

call

call recibe n parámetros.

El primer parámetro al igual que bind, es la referencia a this. Los demás parámetros son los argumentos que se necesitan para llamar la función.

En cuanto a su valor de retorno, call no regresa nada. O bueno regresa lo que la función original tenga definido.

Es importante que tengas en mente sobre todo esta última diferencia entre call y bind.

A diferencia de bind, call no hace una copia de la función con la nueva referencia para this. En cambio manda a llamar a la función en ese mismo momento.

Por lo tanto como ya sospecharás, call no nos funciona para la solución actual

const games = getPrizeGames(3)
games.forEach(function(game) {
    setTimeout(game.getCongratsMessage.call(game, 'Jaime'), 3000);
})

Al correr este código notarás que funciona como si no estuvieramos utilizando setTimeout. Es obvio, en realidad no le estás pasando la funcion que debe correr a setTimeout, la estás ejecutando y pasas como parámetro su valor de retorno, en este caso nada.

apply

apply es muy similar a call, pero en su caso solo recibe 2 parámetors. Como ya supondrás el primero es la nueva referencia para this. El 2° en cambio es un arreglo que contiene n elementos. Estos elementos representan…los parámetros

const games = getPrizeGames(3)
games.forEach(function(game) {
    setTimeout(game.getCongratsMessage.apply(game, ['Jesus']), 3000);
})

Hace exactamente lo mismo que call, solo cambia la sintaxis de los parametros que recibe. Por lo tanto apply también es inútil para esta solución.


Como puedes ver el comportamiento de bind, call y apply no es muy complejo en si mismo, pero es difícil entenderlo sino tienes las bases firmes antes.

Ten en cuenta que el hecho de que call y apply no sean la solución para este ejemplo en específico, no quiere decir que no se puedan utilizar en otros escenarios.

Al final para decidir entre bind y call/apply solo necesitas saber si necesitas la copia de la función con el this modificado o si prefieres solo correr la función modificada una vez y ya.

Y bueno… para decidir entre call y apply…te lo dejo al gusto. Aunque personalmente me gusta mas utilizar apply porque me da mas flexibilidad el poder pasar los argumentos como un arreglo.